import 'reflect-metadata';

type Constructor<T> = new (...args: any[]) => T;
const propMetadataKey = 'jsonProp';
const classMetadataKey = 'classProperties';

export function bindJsonProp(jsonPropSetting: JsonPropMetadata | string): (target: object, propertyKey: string) => void {
    return (target: object, propertyKey: string) => {
        const formattedSetting: JsonPropMetadata = (typeof jsonPropSetting === "string") ? {name: jsonPropSetting} : jsonPropSetting;
        Reflect.defineMetadata(propMetadataKey, formattedSetting, target, propertyKey);

        let properties: string[] = Reflect.getMetadata(classMetadataKey, target);

        if (properties) {
            properties.push(propertyKey);
        } else {
            properties = [propertyKey];
            Reflect.defineMetadata(classMetadataKey, properties, target);
        }
    };
}

export function convertToType<T>(targetType: Constructor<T>): (jsonData: object) => T {
    const target = new targetType() as any;

    return (jsonData: {[key: string]: any}) => {
        const keyMappers = getMetaKeyMappers(targetType, jsonData);
        Object.keys(jsonData).forEach(jsonKey => {
            const keyMapper = keyMappers.find(keyMapper => keyMapper.jsonKey === jsonKey);
            const { typeConverter } = keyMapper?.metadata || {};

            target[keyMapper?.objectKey || jsonKey] = typeConverter?.deserialize(jsonData[jsonKey]) || jsonData[jsonKey];
        });

        return target;
    };
}

export function convertToJson(target: any): { [key: string]: any } {
    return Object.keys(target).reduce((preState: object, key: string) => {
        const { name, typeConverter }: JsonPropMetadata = Reflect.getMetadata(propMetadataKey, target, key) || {};
        return { ...preState, [name || key]: typeConverter?.serialize(target[key]) || target[key] };
    }, {});
}

export const dateStringConverter = {
    serialize: (date: Date) => date.toISOString(),
    deserialize: (dateStr: string) => new Date(dateStr),
}

function getMetaKeyMappers<T>(targetType: Constructor<T>, jsonData: object): KeyMapper[] {
    const target = new targetType() as any;
    const objectKeys = Reflect.getMetadata(classMetadataKey, target) as string[];

    return objectKeys.map(objectKey => {
        const metadata = Reflect.getMetadata(propMetadataKey, target, objectKey);

        return ({
            objectKey,
            jsonKey: getJsonKey(metadata) || objectKey,
            metadata: metadata,
        });
    });
}

function getJsonKey(metadata: JsonPropMetadata): string {
    if (typeof metadata === 'string') {
        return metadata;
    } else {
        return metadata?.name || '';
    }
}

interface JsonPropMetadata {
    name?: string;
    ignored?: boolean;
    typeConverter?: TypeConverter;
}

interface TypeConverter {
    serialize: (a: any) => any;
    deserialize: (a: any) => any;
}

interface KeyMapper {
    objectKey: string;
    jsonKey: string;
    metadata: JsonPropMetadata;
}
